/******************************************************************************* * Copyright (c) 2015 Jeff Martin. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public * License v3.0 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * * Contributors: * Jeff Martin - initial API and implementation ******************************************************************************/ package cuchaz.enigma; import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.StringWriter; import java.util.List; import java.util.Map; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import javassist.CtClass; import javassist.bytecode.Descriptor; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.strobel.assembler.metadata.MetadataSystem; import com.strobel.assembler.metadata.TypeDefinition; import com.strobel.assembler.metadata.TypeReference; import com.strobel.decompiler.DecompilerContext; import com.strobel.decompiler.DecompilerSettings; import com.strobel.decompiler.PlainTextOutput; import com.strobel.decompiler.languages.java.JavaOutputVisitor; import com.strobel.decompiler.languages.java.ast.AstBuilder; import com.strobel.decompiler.languages.java.ast.CompilationUnit; import com.strobel.decompiler.languages.java.ast.InsertParenthesesVisitor; import cuchaz.enigma.analysis.EntryReference; import cuchaz.enigma.analysis.JarClassIterator; import cuchaz.enigma.analysis.JarIndex; import cuchaz.enigma.analysis.SourceIndex; import cuchaz.enigma.analysis.SourceIndexVisitor; import cuchaz.enigma.analysis.Token; import cuchaz.enigma.bytecode.ClassProtectifier; import cuchaz.enigma.bytecode.ClassPublifier; import cuchaz.enigma.mapping.*; public class Deobfuscator { public interface ProgressListener { void init(int totalWork, String title); void onProgress(int numDone, String message); } private JarFile m_jar; private DecompilerSettings m_settings; private JarIndex m_jarIndex; private Mappings m_mappings; private MappingsRenamer m_renamer; private Map<TranslationDirection, Translator> m_translatorCache; public Deobfuscator(JarFile jar) throws IOException { m_jar = jar; // build the jar index m_jarIndex = new JarIndex(); m_jarIndex.indexJar(m_jar, true); // config the decompiler m_settings = DecompilerSettings.javaDefaults(); m_settings.setMergeVariables(true); m_settings.setForceExplicitImports(true); m_settings.setForceExplicitTypeArguments(false); m_settings.setRetainRedundantCasts(false); // m_settings.setShowDebugLineNumbers(true); // DEBUG // m_settings.setShowSyntheticMembers(true); // init defaults m_translatorCache = Maps.newTreeMap(); // init mappings setMappings(new Mappings()); } public JarFile getJar() { return m_jar; } public String getJarName() { return m_jar.getName(); } public JarIndex getJarIndex() { return m_jarIndex; } public Mappings getMappings() { return m_mappings; } public void setMappings(Mappings val) { setMappings(val, true); } public void setMappings(Mappings val, boolean warnAboutDrops) { if(val == null) val = new Mappings(); // drop mappings that don't match the jar MappingsChecker checker = new MappingsChecker(m_jarIndex); checker.dropBrokenMappings(val); if(warnAboutDrops) { for(java.util.Map.Entry<ClassEntry, ClassMapping> mapping : checker .getDroppedClassMappings().entrySet()) System.out.println("WARNING: Couldn't find class entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped."); for(java.util.Map.Entry<ClassEntry, ClassMapping> mapping : checker .getDroppedInnerClassMappings().entrySet()) System.out.println("WARNING: Couldn't find inner class entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped."); for(java.util.Map.Entry<FieldEntry, FieldMapping> mapping : checker .getDroppedFieldMappings().entrySet()) System.out.println("WARNING: Couldn't find field entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped."); for(java.util.Map.Entry<BehaviorEntry, MethodMapping> mapping : checker .getDroppedMethodMappings().entrySet()) System.out.println("WARNING: Couldn't find behavior entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped."); } // check for related method inconsistencies if(checker.getRelatedMethodChecker().hasProblems()) throw new Error( "Related methods are inconsistent! Need to fix the mappings manually.\n" + checker.getRelatedMethodChecker().getReport()); m_mappings = val; m_renamer = new MappingsRenamer(m_jarIndex, val); m_translatorCache.clear(); } public Translator getTranslator(TranslationDirection direction) { Translator translator = m_translatorCache.get(direction); if(translator == null) { translator = m_mappings.getTranslator(direction, m_jarIndex.getTranslationIndex()); m_translatorCache.put(direction, translator); } return translator; } public void getSeparatedClasses(List<ClassEntry> obfClasses, List<ClassEntry> deobfClasses) { for(ClassEntry obfClassEntry : m_jarIndex.getObfClassEntries()) { // skip inner classes if(obfClassEntry.isInnerClass()) continue; // separate the classes ClassEntry deobfClassEntry = deobfuscateEntry(obfClassEntry); if(!deobfClassEntry.equals(obfClassEntry)) // if the class has a mapping, clearly it's deobfuscated deobfClasses.add(deobfClassEntry); else if(!obfClassEntry.getPackageName().equals( Constants.NonePackage)) // also call it deobufscated if it's not in the none package deobfClasses.add(obfClassEntry); else // otherwise, assume it's still obfuscated obfClasses.add(obfClassEntry); } } public CompilationUnit getSourceTree(String className) { // we don't know if this class name is obfuscated or deobfuscated // we need to tell the decompiler the deobfuscated name so it doesn't // get freaked out // the decompiler only sees classes after deobfuscation, so we need to // load it by the deobfuscated name if there is one // first, assume class name is deobf String deobfClassName = className; // if it wasn't actually deobf, then we can find a mapping for it and // get the deobf name ClassMapping classMapping = m_mappings.getClassByObf(className); if(classMapping != null && classMapping.getDeobfName() != null) deobfClassName = classMapping.getDeobfName(); // set the type loader TranslatingTypeLoader loader = new TranslatingTypeLoader(m_jar, m_jarIndex, getTranslator(TranslationDirection.Obfuscating), getTranslator(TranslationDirection.Deobfuscating)); m_settings.setTypeLoader(loader); // see if procyon can find the type TypeReference type = new MetadataSystem(loader).lookupType(deobfClassName); if(type == null) throw new Error(String.format( "Unable to find type: %s (deobf: %s)\nTried class names: %s", className, deobfClassName, loader.getClassNamesToTry(deobfClassName))); TypeDefinition resolvedType = type.resolve(); // decompile it! DecompilerContext context = new DecompilerContext(); context.setCurrentType(resolvedType); context.setSettings(m_settings); AstBuilder builder = new AstBuilder(context); builder.addType(resolvedType); builder.runTransformations(null); return builder.getCompilationUnit(); } public SourceIndex getSourceIndex(CompilationUnit sourceTree, String source) { return getSourceIndex(sourceTree, source, null); } public SourceIndex getSourceIndex(CompilationUnit sourceTree, String source, Boolean ignoreBadTokens) { // build the source index SourceIndex index; if(ignoreBadTokens != null) index = new SourceIndex(source, ignoreBadTokens); else index = new SourceIndex(source); sourceTree.acceptVisitor(new SourceIndexVisitor(), index); // DEBUG // sourceTree.acceptVisitor( new TreeDumpVisitor( new File( "tree.txt" ) // ), null ); // resolve all the classes in the source references for(Token token : index.referenceTokens()) { EntryReference<Entry, Entry> deobfReference = index.getDeobfReference(token); // get the obfuscated entry Entry obfEntry = obfuscateEntry(deobfReference.entry); // try to resolve the class ClassEntry resolvedObfClassEntry = m_jarIndex.getTranslationIndex().resolveEntryClass(obfEntry); if(resolvedObfClassEntry != null && !resolvedObfClassEntry.equals(obfEntry.getClassEntry())) { // change the class of the entry obfEntry = obfEntry.cloneToNewClass(resolvedObfClassEntry); // save the new deobfuscated reference deobfReference.entry = deobfuscateEntry(obfEntry); index.replaceDeobfReference(token, deobfReference); } // DEBUG // System.out.println( token + " -> " + reference + " -> " + // index.getReferenceToken( reference ) ); } return index; } public String getSource(CompilationUnit sourceTree) { // render the AST into source StringWriter buf = new StringWriter(); sourceTree.acceptVisitor(new InsertParenthesesVisitor(), null); sourceTree.acceptVisitor(new JavaOutputVisitor( new PlainTextOutput(buf), m_settings), null); return buf.toString(); } public void writeSources(File dirOut, ProgressListener progress) throws IOException { // get the classes to decompile Set<ClassEntry> classEntries = Sets.newHashSet(); for(ClassEntry obfClassEntry : m_jarIndex.getObfClassEntries()) { // skip inner classes if(obfClassEntry.isInnerClass()) continue; classEntries.add(obfClassEntry); } if(progress != null) progress.init(classEntries.size(), "Decompiling classes..."); // DEOBFUSCATE ALL THE THINGS!! @_@ int i = 0; for(ClassEntry obfClassEntry : classEntries) { ClassEntry deobfClassEntry = deobfuscateEntry(new ClassEntry(obfClassEntry)); if(progress != null) progress.onProgress(i++, deobfClassEntry.toString()); try { // get the source String source = getSource(getSourceTree(obfClassEntry.getName())); // write the file File file = new File(dirOut, deobfClassEntry.getName() .replace('.', '/') + ".java"); file.getParentFile().mkdirs(); try(FileWriter out = new FileWriter(file)) { out.write(source); } }catch(Throwable t) { throw new Error("Unable to deobfuscate class " + deobfClassEntry.toString() + " (" + obfClassEntry.toString() + ")", t); } } if(progress != null) progress.onProgress(i, "Done!"); } public void writeJar(File out, ProgressListener progress) { final TranslatingTypeLoader loader = new TranslatingTypeLoader(m_jar, m_jarIndex, getTranslator(TranslationDirection.Obfuscating), getTranslator(TranslationDirection.Deobfuscating)); transformJar(out, progress, new ClassTransformer() { @Override public CtClass transform(CtClass c) throws Exception { return loader.transformClass(c); } }); } public void protectifyJar(File out, ProgressListener progress) { transformJar(out, progress, new ClassTransformer() { @Override public CtClass transform(CtClass c) throws Exception { return ClassProtectifier.protectify(c); } }); } public void publifyJar(File out, ProgressListener progress) { transformJar(out, progress, new ClassTransformer() { @Override public CtClass transform(CtClass c) throws Exception { return ClassPublifier.publify(c); } }); } private interface ClassTransformer { public CtClass transform(CtClass c) throws Exception; } private void transformJar(File out, ProgressListener progress, ClassTransformer transformer) { try(JarOutputStream outJar = new JarOutputStream(new FileOutputStream(out))) { if(progress != null) progress.init(JarClassIterator.getClassEntries(m_jar).size(), "Transforming classes..."); int i = 0; for(CtClass c : JarClassIterator.classes(m_jar)) { if(progress != null) progress.onProgress(i++, c.getName()); try { c = transformer.transform(c); outJar.putNextEntry(new JarEntry(c.getName().replace('.', '/') + ".class")); outJar.write(c.toBytecode()); outJar.closeEntry(); }catch(Throwable t) { throw new Error("Unable to transform class " + c.getName(), t); } } if(progress != null) progress.onProgress(i, "Done!"); outJar.close(); }catch(IOException ex) { throw new Error("Unable to write to Jar file!"); } } public <T extends Entry> T obfuscateEntry(T deobfEntry) { if(deobfEntry == null) return null; return getTranslator(TranslationDirection.Obfuscating).translateEntry( deobfEntry); } public <T extends Entry> T deobfuscateEntry(T obfEntry) { if(obfEntry == null) return null; return getTranslator(TranslationDirection.Deobfuscating) .translateEntry(obfEntry); } public <E extends Entry, C extends Entry> EntryReference<E, C> obfuscateReference( EntryReference<E, C> deobfReference) { if(deobfReference == null) return null; return new EntryReference<E, C>(obfuscateEntry(deobfReference.entry), obfuscateEntry(deobfReference.context), deobfReference); } public <E extends Entry, C extends Entry> EntryReference<E, C> deobfuscateReference( EntryReference<E, C> obfReference) { if(obfReference == null) return null; return new EntryReference<E, C>(deobfuscateEntry(obfReference.entry), deobfuscateEntry(obfReference.context), obfReference); } public boolean isObfuscatedIdentifier(Entry obfEntry) { if(obfEntry instanceof MethodEntry) { // HACKHACK: Object methods are not obfuscated identifiers MethodEntry obfMethodEntry = (MethodEntry)obfEntry; String name = obfMethodEntry.getName(); String sig = obfMethodEntry.getSignature().toString(); if(name.equals("clone") && sig.equals("()Ljava/lang/Object;")) return false; else if(name.equals("equals") && sig.equals("(Ljava/lang/Object;)Z")) return false; else if(name.equals("finalize") && sig.equals("()V")) return false; else if(name.equals("getClass") && sig.equals("()Ljava/lang/Class;")) return false; else if(name.equals("hashCode") && sig.equals("()I")) return false; else if(name.equals("notify") && sig.equals("()V")) return false; else if(name.equals("notifyAll") && sig.equals("()V")) return false; else if(name.equals("toString") && sig.equals("()Ljava/lang/String;")) return false; else if(name.equals("wait") && sig.equals("()V")) return false; else if(name.equals("wait") && sig.equals("(J)V")) return false; else if(name.equals("wait") && sig.equals("(JI)V")) return false; } return m_jarIndex.containsObfEntry(obfEntry); } public boolean isRenameable(EntryReference<Entry, Entry> obfReference) { return obfReference.isNamed() && isObfuscatedIdentifier(obfReference.getNameableEntry()); } // NOTE: these methods are a bit messy... oh well public boolean hasDeobfuscatedName(Entry obfEntry) { Translator translator = getTranslator(TranslationDirection.Deobfuscating); if(obfEntry instanceof ClassEntry) { ClassEntry obfClass = (ClassEntry)obfEntry; List<ClassMapping> mappingChain = m_mappings.getClassMappingChain(obfClass); ClassMapping classMapping = mappingChain.get(mappingChain.size() - 1); return classMapping != null && classMapping.getDeobfName() != null; }else if(obfEntry instanceof FieldEntry) return translator.translate((FieldEntry)obfEntry) != null; else if(obfEntry instanceof MethodEntry) return translator.translate((MethodEntry)obfEntry) != null; else if(obfEntry instanceof ConstructorEntry) // constructors have no names return false; else if(obfEntry instanceof ArgumentEntry) return translator.translate((ArgumentEntry)obfEntry) != null; else throw new Error("Unknown entry type: " + obfEntry.getClass().getName()); } public void rename(Entry obfEntry, String newName) { if(obfEntry instanceof ClassEntry) m_renamer.setClassName((ClassEntry)obfEntry, Descriptor.toJvmName(newName)); else if(obfEntry instanceof FieldEntry) m_renamer.setFieldName((FieldEntry)obfEntry, newName); else if(obfEntry instanceof MethodEntry) m_renamer.setMethodTreeName((MethodEntry)obfEntry, newName); else if(obfEntry instanceof ConstructorEntry) throw new IllegalArgumentException("Cannot rename constructors"); else if(obfEntry instanceof ArgumentEntry) m_renamer.setArgumentName((ArgumentEntry)obfEntry, newName); else throw new Error("Unknown entry type: " + obfEntry.getClass().getName()); // clear caches m_translatorCache.clear(); } public void removeMapping(Entry obfEntry) { if(obfEntry instanceof ClassEntry) m_renamer.removeClassMapping((ClassEntry)obfEntry); else if(obfEntry instanceof FieldEntry) m_renamer.removeFieldMapping((FieldEntry)obfEntry); else if(obfEntry instanceof MethodEntry) m_renamer.removeMethodTreeMapping((MethodEntry)obfEntry); else if(obfEntry instanceof ConstructorEntry) throw new IllegalArgumentException("Cannot rename constructors"); else if(obfEntry instanceof ArgumentEntry) m_renamer.removeArgumentMapping((ArgumentEntry)obfEntry); else throw new Error("Unknown entry type: " + obfEntry); // clear caches m_translatorCache.clear(); } public void markAsDeobfuscated(Entry obfEntry) { if(obfEntry instanceof ClassEntry) m_renamer.markClassAsDeobfuscated((ClassEntry)obfEntry); else if(obfEntry instanceof FieldEntry) m_renamer.markFieldAsDeobfuscated((FieldEntry)obfEntry); else if(obfEntry instanceof MethodEntry) m_renamer.markMethodTreeAsDeobfuscated((MethodEntry)obfEntry); else if(obfEntry instanceof ConstructorEntry) throw new IllegalArgumentException("Cannot rename constructors"); else if(obfEntry instanceof ArgumentEntry) m_renamer.markArgumentAsDeobfuscated((ArgumentEntry)obfEntry); else throw new Error("Unknown entry type: " + obfEntry); // clear caches m_translatorCache.clear(); } }